Conditional types很有意思,因為TypeScript有些很好用的utility types其實是用conditional types定義出來的。
但這是菜鳥系列,所以這篇不會證明如何用conditional types定義utility types,有興趣的人可以參考這篇文章,而這裡還是乖乖從TypeScript官方文件開始入手吧。
Condition types的語法非常類似於JavaScript的三元運算子(ternary operator),語法如下:
type ConditionType = T extends U ? X : Y;
若T型別是從U型別延伸(extends)出來的,則ConditionType會是X型別;否則的話,ConditionType會是Y型別。
例如:
interface Animal {
specie: string;
w: number;
h: number;
}
interface Human extends Animal {
name: string;
age: number;
}
interface Flower {
variety: string;
color: string;
}
type H = Human extends Animal ? string : unknown; // type H = string
type F = Flower extends Animal ? string : unknown; // type F = unknown
但像上面這樣的hard code實在無法顯示出conditional types的強大,先將第一個範例改寫成generic形式:
type isHuman<T> = T extends Human? Human : unknown;
const xiaMing: Human = {specie: "human", w: 70, h: 175, name: 'xia ming', age: 25};
type Ming = isHuman<typeof xiaMing>; // type Ming = Human
寫成generic形式之後,可以嘗試將conditional types和其他關鍵字、運算子或是取得型別的方式,例如和昨天學的indexed accessed types一起使用:
type Str<T> = T extends {name: string}? T["name"] : never;
const xiaMing = {specie: "human", w: 70, h: 175, name: 'xia ming', age: 25};
type M = Str<typeof xiaMing>; // type M = string
或是加入 infer
關鍵字去推導型別,讓conditional types的使用方式更彈性:
// ex1.
type arrElem<T> = T extends Array<infer Elem> ? Elem : never;
type N1 = arrElem<Array<number>>; // type N1 = number
type N2 = arrElem<number[]>; // type N2 = number
type Never = arrElem<number>; // type N3 = never
// ex2.
type getReturn<T> = T extends (...args: any) => infer Return ? Return : never;
type num = getReturn<()=>number>;
此外conditional types的語法也可以像三元運算子一樣,將多個conditional types串聯起來:
type WhatT<T> = T extends Human ? Human : T extends Animal ? Animal : never;
const dog = {specie: "dog", w: 28, h: 30};
type D = WhatT<typeof dog>; // type D = Animal
最後要認識conditional types的 distributive 特性,而最一開始說的utility types就是利用這個特性實作出來的。
先回到最基本的conditional types語法,distributive會發生在型別T是union的時候:
type ConditionType = T extends U ? X : Y;
來看文件的例子,測試一下當型別T是union types的時候會發生什麼事,:
type UnionT<T> = T extends any ? T[] : never;
type U = UnionT<string | number>; // type U = "string"[] | "number"[]
// 也就是
const stringArr: U = ["string", "string"]; // ok
const numberArr: U = ["number", "number"]; // ok
型別U會是 string[] | number[]
的原因在於TypeScript會把conditional types變成是 "union" 的 conditional types,意思是:
type U = UnionT<string | number> extends any ? T[] : never;
= UnionT<string> extends any | UnionT<number> extends any ? T[] : never;
= "string"[] | "number"[]
因為 UnionT<"string">
或 UnionT<"number">
各自確實是 any
型別的子集,所以compiler會判斷為true,也就因此得到 "string"[]
或者 "number"[]
型別。
但如果是希望得到的是 [string | number]
型別,需要將T型別加上 []
,才能避開distributive:
type UnionT<T> = [T] extends any ? T[] : never;
type U = UnionT<string | number>; // type U = string[] | number[]
const arr = [1, 2, 3, "hello", "world"]; // ok
同樣的,另一種array寫法也是要加上 Array<>
:
type UnionT<T> = Array<T> extends any ? Array<T> : never;
type U = UnionT<string | number>;
const arr = [1, 2, 3, "hello", "world"]; // ok
以上方式讓compiler知道要處理的是陣列元素的型別,也就能取得預期中的型別了~
參考資料
Conditional Types @TypeScript Handbook
Using TypeScript Conditional Types Like a Pro
Power of conditional types in Typescript